﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Utilities;


namespace PostscriptTools.Intellisense {

	[Export(typeof(IViewTaggerProvider))]
	[ContentType("postscript")]
	[TagType(typeof(TextMarkerTag))]
	internal class BraceMatchingTaggerProvider : IViewTaggerProvider {
		[Import]
		internal IBufferTagAggregatorFactoryService AggregatorFactory = null;

		public ITagger<T> CreateTagger<T>(ITextView textView, ITextBuffer buffer) where T : ITag {
			if (textView == null) {
				return null;
			}
			//provide highlighting only on the top-level buffer
			if (textView.TextBuffer != buffer) {
				return null;
			}
			// create a single tagger for each buffer.
			Func<ITagger<T>> sc = delegate() { return new BraceMatchingTagger(textView, buffer, this.AggregatorFactory) as ITagger<T>; };
			return buffer.Properties.GetOrCreateSingletonProperty<ITagger<T>>(sc);
		}

	}


	internal class BraceMatchingTagger : ITagger<TextMarkerTag> {
		ITextView View { get; set; }
		ITextBuffer Buffer { get; set; }
		SnapshotPoint? CurrentChar { get; set; } // where our caret is pointing at in the current buffer
		private readonly Dictionary<char, char> _braceCharList;
		private readonly Dictionary<PostscriptToken, PostscriptToken> _braceTokenList;
		private readonly Dictionary<string, string> _braceOperatorList;
		readonly ITagAggregator<PostscriptTokenTag> _aggregator;

		internal BraceMatchingTagger(ITextView view, ITextBuffer sourceBuffer, IBufferTagAggregatorFactoryService aggregatorFactory) {
			//here the keys are the open braces, and the values are the close braces
			this._braceCharList = new Dictionary<char, char>();
			this._braceCharList.Add('{', '}');
			this._braceCharList.Add('[', ']');
			this._braceCharList.Add('(', ')');
			this._braceCharList.Add('<', '>');
			this._braceTokenList = new Dictionary<PostscriptToken, PostscriptToken>();
			this._braceTokenList.Add(PostscriptToken.PsDictOpenOperator, PostscriptToken.PsDictCloseOperator); // << and >>
			this._braceOperatorList = new Dictionary<string, string>(StringComparer.Ordinal);
			this._braceOperatorList.Add("gsave", "grestore");
			this._braceOperatorList.Add("begin", "end");
			View = view;
			Buffer = sourceBuffer;
			CurrentChar = null;
			_aggregator = aggregatorFactory.CreateTagAggregator<PostscriptTokenTag>(sourceBuffer);

			View.Caret.PositionChanged += CaretPositionChanged;
			View.LayoutChanged += ViewLayoutChanged;
		}

#pragma warning disable 67
		public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
#pragma warning restore

		void ViewLayoutChanged(object sender, TextViewLayoutChangedEventArgs e) {
			if (e.NewSnapshot != e.OldSnapshot) { //make sure that there has really been a change
				UpdateAtCaretPosition(View.Caret.Position);
			}
		}

		void CaretPositionChanged(object sender, CaretPositionChangedEventArgs e) {
			UpdateAtCaretPosition(e.NewPosition);
		}

		void UpdateAtCaretPosition(CaretPosition caretPosition) {
			CurrentChar = caretPosition.Point.GetPoint(this.Buffer, caretPosition.Affinity);
			if (!CurrentChar.HasValue) {
				return;
			}
			var tempEvent = TagsChanged;
			if (tempEvent != null) {
				tempEvent(this, new SnapshotSpanEventArgs(new SnapshotSpan(this.Buffer.CurrentSnapshot, 0, this.Buffer.CurrentSnapshot.Length)));
			}
		}

		private const int PARSING_DELAY = 500;
		private Stopwatch GetTagsLastRunTime;
		public IEnumerable<ITagSpan<TextMarkerTag>> GetTags(NormalizedSnapshotSpanCollection spans) {
			// a TextMarkerTag is associated with an adornment
			// a NormalizedSnapshotSpanCollection is A read-only collection of SnapshotSpan objects, all from the same snapshot. The snapshot spans are sorted by start position. Adjacent and overlapping spans are combined
			if (spans.Count == 0 || Buffer.CurrentSnapshot.Length == 0)   //there is no content in the buffer
				yield break;
			//don't do anything if the current SnapshotPoint is not initialized or at the end of the buffer
			if (!CurrentChar.HasValue || CurrentChar.Value.Position >= CurrentChar.Value.Snapshot.Length) {
				yield break;
			}
			// we do not want to be called more than twice per second. Ignore superfluous requests because it slows down the editor too much
			if (GetTagsLastRunTime == null) {
				GetTagsLastRunTime = Stopwatch.StartNew();
			} else {
				if (GetTagsLastRunTime.ElapsedMilliseconds < PARSING_DELAY) {
					GetTagsLastRunTime.Restart();
					yield break;
				} else {
					GetTagsLastRunTime.Restart();
				}
			}
			//hold on to a snapshot of the current character
			SnapshotPoint currentChar = CurrentChar.Value;
			//if the requested snapshot isn't the same as the one the brace is on, translate our spans to the expected snapshot
			if (spans[0].Snapshot != currentChar.Snapshot) {
				currentChar = currentChar.TranslateTo(spans[0].Snapshot, PointTrackingMode.Positive); // Translates this snapshot point to a different snapshot of the same ITextBuffer
			}
			// now our currentChar is interpreted relative to the spans argument
			// get the token under the cursor
			var pairSpan = new SnapshotSpan();
			IEnumerable<IMappingTagSpan<PostscriptTokenTag>> allPostscriptTokenTags = _aggregator.GetTags(spans);
			int tokenCounter = 0;
			foreach (IMappingTagSpan<PostscriptTokenTag> tagSpan in allPostscriptTokenTags) {
				tokenCounter++;
				NormalizedSnapshotSpanCollection tagSpans = tagSpan.Span.GetSpans(spans[0].Snapshot); // Maps the IMappingSpan to our ITextSnapshot 
				foreach (SnapshotSpan snapshotSpan in tagSpans) {
					if (snapshotSpan.Contains(currentChar)) {
						PostscriptToken currenttoken = tagSpan.Tag.Type;
						if (this._braceTokenList.ContainsKey(currenttoken)) {
							// aha, we need to look forward for the endtag
							// only supply the postscripttokentags from currentChar
							var tokensFromHereOn = allPostscriptTokenTags.Skip(tokenCounter - 1);
							if (FindMatchingCloseToken(currentChar, currenttoken, this._braceTokenList[currenttoken], tokensFromHereOn, out pairSpan)) {
								yield return new TagSpan<TextMarkerTag>(snapshotSpan, new TextMarkerTag("bracehighlight"));
								yield return new TagSpan<TextMarkerTag>(pairSpan, new TextMarkerTag("bracehighlight"));
								yield break;
							}
						} else if (this._braceTokenList.ContainsValue(currenttoken)) {
							// aha, we need to look backwards for the opentag
							var opentoken = from n in this._braceTokenList
										where n.Value.Equals(currenttoken)
										select n.Key;
							var tokensTillHere = allPostscriptTokenTags.Take(tokenCounter);
							if (FindMatchingOpenToken(currentChar, opentoken.ElementAt(0), currenttoken, tokensTillHere, out pairSpan)) {
								yield return new TagSpan<TextMarkerTag>(snapshotSpan, new TextMarkerTag("bracehighlight"));
								yield return new TagSpan<TextMarkerTag>(pairSpan, new TextMarkerTag("bracehighlight"));
								yield break;
							}
						} else if (currenttoken == PostscriptToken.PsOperator) {
							var operatortag = tagSpan.Tag as PostscriptNamedTag;
							if (this._braceOperatorList.ContainsKey(operatortag.Name)) {
								// aha, we need to look forward for the operator with the endname
								var tokensFromHereOn = allPostscriptTokenTags.Skip(tokenCounter - 1);
								if (FindMatchingCloseToken(currentChar, operatortag.Name, this._braceOperatorList[operatortag.Name], tokensFromHereOn, out pairSpan)) {
									yield return new TagSpan<TextMarkerTag>(snapshotSpan, new TextMarkerTag("bracehighlight"));
									yield return new TagSpan<TextMarkerTag>(pairSpan, new TextMarkerTag("bracehighlight"));
									yield break;
								}
							} else if (this._braceOperatorList.ContainsValue(operatortag.Name)) {
								// aha, we need to look backwards for the opentag
								var opentoken = from n in this._braceOperatorList
								                where n.Value.Equals(operatortag.Name, StringComparison.Ordinal)
								                select n.Key;
								var tokensTillHere = allPostscriptTokenTags.Take(tokenCounter);
								if (FindMatchingOpenToken(currentChar, opentoken.ElementAt(0), operatortag.Name, tokensTillHere, out pairSpan)) {
									yield return new TagSpan<TextMarkerTag>(snapshotSpan, new TextMarkerTag("bracehighlight"));
									yield return new TagSpan<TextMarkerTag>(pairSpan, new TextMarkerTag("bracehighlight"));
									yield break;
								}
							}
						}
					}
				}
			}
			// ok , look for char based stuff
			// get the current char and the previous char
			char currentText = currentChar.GetChar();
			SnapshotPoint lastChar = currentChar == 0 ? currentChar : currentChar - 1; //if currentChar is 0 (beginning of buffer), don't move it back
			char lastText = lastChar.GetChar();

			if (this._braceCharList.ContainsKey(currentText)) {   //the key is the open brace
				char closeChar;
				this._braceCharList.TryGetValue(currentText, out closeChar);
				if (FindMatchingCloseChar(currentChar, currentText, closeChar, View.TextViewLines.Count, out pairSpan)) {
					yield return new TagSpan<TextMarkerTag>(new SnapshotSpan(currentChar, 1), new TextMarkerTag("bracehighlight"));
					yield return new TagSpan<TextMarkerTag>(pairSpan, new TextMarkerTag("bracehighlight"));
					yield break;
				}
			} else if (this._braceCharList.ContainsValue(lastText)) { //the value is the close brace, which is the *previous* character 
				var open = from n in this._braceCharList
						   where n.Value.Equals(lastText)
						   select n.Key;
				if (FindMatchingOpenChar(lastChar, (char)open.ElementAt<char>(0), lastText, View.TextViewLines.Count, out pairSpan)) {
					yield return new TagSpan<TextMarkerTag>(new SnapshotSpan(lastChar, 1), new TextMarkerTag("bracehighlight"));
					yield return new TagSpan<TextMarkerTag>(pairSpan, new TextMarkerTag("bracehighlight"));
					yield break;
				}
			}
		}

		private static bool FindMatchingCloseToken(SnapshotPoint startPoint, PostscriptToken openingToken, PostscriptToken closingToken, IEnumerable<IMappingTagSpan<PostscriptTokenTag>> alltokens, out SnapshotSpan pairSpan) {
			pairSpan = new SnapshotSpan(startPoint.Snapshot, 1, 1);
			int openCount = 0;
			bool looking = false;
			foreach (IMappingTagSpan<PostscriptTokenTag> tagSpan in alltokens) {
				NormalizedSnapshotSpanCollection tagSpans = tagSpan.Span.GetSpans(startPoint.Snapshot); // Maps the IMappingSpan to our ITextSnapshot 
				foreach (SnapshotSpan snapshotSpan in tagSpans) {
					if (looking && (tagSpan.Tag.Type == closingToken)) {
						if (openCount > 0) {
							openCount--;
						} else {
							//found the matching closingToken
							pairSpan = snapshotSpan;
							return true;
						}
					} else if (looking && (tagSpan.Tag.Type == openingToken)) {
						openCount++;
					}
					if (snapshotSpan.Contains(startPoint)) {
						looking = true;
					}
				}
			}
			return false;
		}

		private static bool FindMatchingCloseToken(SnapshotPoint startPoint, string openingOperandName, string closingOperandName, IEnumerable<IMappingTagSpan<PostscriptTokenTag>> alltokens, out SnapshotSpan pairSpan) {
			pairSpan = new SnapshotSpan(startPoint.Snapshot, 1, 1);
			int openCount = 0;
			bool looking = false;
			foreach (IMappingTagSpan<PostscriptTokenTag> tagSpan in alltokens) {
				NormalizedSnapshotSpanCollection tagSpans = tagSpan.Span.GetSpans(startPoint.Snapshot); // Maps the IMappingSpan to our ITextSnapshot 
				foreach (SnapshotSpan snapshotSpan in tagSpans) {
					if (looking && (tagSpan.Tag.Type == PostscriptToken.PsOperator)){
						var op = tagSpan.Tag as PostscriptNamedTag;
						if ((op != null) && (op.Name.Equals(closingOperandName, StringComparison.Ordinal))) {
							if (openCount > 0) {
								openCount--;
							} else {
								//found the matching closingToken
								pairSpan = snapshotSpan;
								return true;
							}
						} else if ((op != null) && (op.Name.Equals(openingOperandName, StringComparison.Ordinal))) {
							openCount++;
						}
					}
					if (snapshotSpan.Contains(startPoint)) {
						looking = true;
					}
				}
			}
			return false;
		}

		private static bool FindMatchingOpenToken(SnapshotPoint startPoint, PostscriptToken openingToken, PostscriptToken closingToken, IEnumerable<IMappingTagSpan<PostscriptTokenTag>> alltokens, out SnapshotSpan pairSpan) {
			pairSpan = new SnapshotSpan(startPoint.Snapshot, 1, 1);
			int closeCount = 0;
			bool looking = false;
			foreach (IMappingTagSpan<PostscriptTokenTag> tagSpan in alltokens.Reverse()) {
				NormalizedSnapshotSpanCollection tagSpans = tagSpan.Span.GetSpans(startPoint.Snapshot); // Maps the IMappingSpan to our ITextSnapshot 
				foreach (SnapshotSpan snapshotSpan in tagSpans) {
					if (looking && (tagSpan.Tag.Type == openingToken)) {
						if (closeCount > 0) {
							closeCount--;
						} else {
							//found the matching openingToken
							pairSpan = snapshotSpan;
							return true;
						}
					} else if (looking && (tagSpan.Tag.Type == closingToken)) {
						closeCount++;
					}
					if (snapshotSpan.Contains(startPoint)) {
						looking = true;
					}
				}
			}
			return false;
		}
		private static bool FindMatchingOpenToken(SnapshotPoint startPoint, string openingOperandName, string closingOperandName, IEnumerable<IMappingTagSpan<PostscriptTokenTag>> alltokens, out SnapshotSpan pairSpan) {
			pairSpan = new SnapshotSpan(startPoint.Snapshot, 1, 1);
			int closeCount = 0;
			bool looking = false;
			foreach (IMappingTagSpan<PostscriptTokenTag> tagSpan in alltokens.Reverse()) {
				NormalizedSnapshotSpanCollection tagSpans = tagSpan.Span.GetSpans(startPoint.Snapshot); // Maps the IMappingSpan to our ITextSnapshot 
				foreach (SnapshotSpan snapshotSpan in tagSpans) {
					if (looking && (tagSpan.Tag.Type == PostscriptToken.PsOperator)) {
						var op = tagSpan.Tag as PostscriptNamedTag;
						if ((op != null) && (op.Name.Equals(openingOperandName, StringComparison.Ordinal))) {
							if (closeCount > 0) {
								closeCount--;
							} else {
								//found the matching closingToken
								pairSpan = snapshotSpan;
								return true;
							}
						} else if ((op != null) && (op.Name.Equals(closingOperandName, StringComparison.Ordinal))) {
							closeCount++;
						}
					}
					if (snapshotSpan.Contains(startPoint)) {
						looking = true;
					}
				}
			}
			return false;
		}


		private static bool FindMatchingCloseChar(SnapshotPoint startPoint, char openingChar, char closingChar, int maxLines, out SnapshotSpan pairSpan) {
			pairSpan = new SnapshotSpan(startPoint.Snapshot, 1, 1);
			ITextSnapshotLine line = startPoint.GetContainingLine();
			string lineText = line.GetText();
			int lineNumber = line.LineNumber;
			int offset = startPoint.Position - line.Start.Position + 1;

			int stopLineNumber = startPoint.Snapshot.LineCount - 1;
			if (maxLines > 0) {
				stopLineNumber = Math.Min(stopLineNumber, lineNumber + maxLines);
			}
			int openCount = 0;
			while (true) {
				//walk the entire line
				while (offset < line.Length) {
					char currentChar = lineText[offset];
					if ((openingChar == '(') && (currentChar == '\\')) {
						offset++; // skip over this backslash, and at the end over the escaped char
					} else {
						if (currentChar == closingChar) {
							//found the close character
							if (openCount > 0) {
								openCount--;
							} else {
								//found the matching closingChar
								pairSpan = new SnapshotSpan(startPoint.Snapshot, line.Start + offset, 1);
								return true;
							}
						} else if (currentChar == openingChar) {
							// this is another openingChar
							openCount++;
						}
					}
					offset++;
				}
				//move on to the next line
				if (++lineNumber > stopLineNumber) {
					break;
				}
				line = line.Snapshot.GetLineFromLineNumber(lineNumber);
				lineText = line.GetText();
				offset = 0;
			}
			return false;
		}
		private static bool FindMatchingOpenChar(SnapshotPoint startPoint, char openingChar, char closingChar, int maxLines, out SnapshotSpan pairSpan) {
			pairSpan = new SnapshotSpan(startPoint, startPoint);
			ITextSnapshotLine line = startPoint.GetContainingLine();
			int lineNumber = line.LineNumber;
			int offset = startPoint - line.Start - 1; //move the offset to the character before this one
			//if the offset is negative, move to the previous line
			if (offset < 0) {
				line = line.Snapshot.GetLineFromLineNumber(--lineNumber);
				offset = line.Length - 1;
			}
			string lineText = line.GetText();
			int stopLineNumber = 0;
			if (maxLines > 0) {
				stopLineNumber = Math.Max(stopLineNumber, lineNumber - maxLines);
			}
			int closeCount = 0;
			while (true) {
				// Walk the entire line backwards
				while (offset >= 0) {
					char currentChar = lineText[offset];
					if ((closingChar == ')') && ((currentChar == '(') || (currentChar == ')'))) { // while searching strings, skip over escaped sequences
						if ((offset > 0) && (lineText[offset - 1] == '\\')) {
							offset -= 2; // skip over \) or \(
						}
						if (offset < 0) {
							break;
						} else {
							currentChar = lineText[offset];
						}
					}
					if (currentChar == openingChar) {
						if (closeCount > 0) {
							closeCount--;
						} else {// We've found the open character
							pairSpan = new SnapshotSpan(line.Start + offset, 1); //we just want the character itself
							return true;
						}
					} else if (currentChar == closingChar) {
						closeCount++;
					}
					offset--;
				}
				// Move to the previous line
				if (--lineNumber < stopLineNumber) {
					break;
				}
				line = line.Snapshot.GetLineFromLineNumber(lineNumber);
				lineText = line.GetText();
				offset = line.Length - 1;
			}
			return false;
		}

	}
}
